package org.softee.extractor.sax.helper;

import static org.softee.extractor.sax.helper.NsBindings.extractPrefix;

import org.softee.extractor.helper.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
/**
 * For similar implementation, see http://fossies.org/unix/misc/gcc-4.5.3.tar.gz/dox/XMLWriter_8java_source.html
 * @author morten.hattesen@gmail.com
 *
 */
public class XmlStringBuilder implements ResultBuildingContentHandler<String> {
    private static final Logger logger = new Logger(XmlStringBuilder.class);
    private static final int XML_BUFFER_RESERVE = 1024;
    private NsBindings globalNamespaces;
    private NsBindings contextNamespaces;
    private StringBuilder xml;
    private boolean withinCData;
    private boolean unclosedStartTag;

    public XmlStringBuilder() {
        log("new()");
        globalNamespaces = new NsBindings();
    }

    @Override
    public String getResult() {
        log("Result: %s", xml);
        return xml.toString();
    }

    @Override
    public void startDocument() throws SAXException {
        log("startDocument()");
        contextNamespaces = null;
        xml = new StringBuilder(XML_BUFFER_RESERVE);
    }

    @Override
    public void endDocument() throws SAXException {
        log("endDocument()");
        // don't null the document, since it needs to be recoverable after endDocument()
    }

    @Override
    public void processingInstruction(String target, String data) {
        log("processingInstruction('%s', '%s')", target, data);

    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) {
        log("startElement('%s', '%s', '%s')", uri, localName, qName);
        // push a new context namespace mapping
        contextNamespaces = new NsBindings(contextNamespaces);
        appendStartTag(uri, qName, atts);
    }

    private void appendStartTag(String uri, String qName, Attributes atts) {
        xml.append('<').append(qName);
        for (int i = 0; i < atts.getLength(); i++) {
            String attQName = atts.getQName(i);
            String prefix = NsBindings.extractPrefix(attQName);
            String attValue = atts.getValue(i);
            if (!XmlConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
                // Some parsers (StAX) report xmlns:xx prefix declarations as attributes to the content handler
                log(" +attribute: %s='%s'", attQName, attValue);
                if (!prefix.isEmpty()) {
                    appendNsBindingForQName(attQName);
                }
                appendAttribute(attQName, attValue);
            }
        }
        // ns binding of the element (last to mimick Xerces/Xalan)
        if (!XmlConstants.NULL_NS_URI.equals(uri)) {
            appendNsBindingForQName(qName);
        }
        unclosedStartTag = true;
    }

    @Override
    public void endElement(String uri, String localName, String qName) {
        log("endElement('%s', '%s', '%s')", uri, localName, qName);
        appendEndTag(qName);
        // move the namespace context up to the parent
        contextNamespaces = contextNamespaces.getParent();
    }

    private void appendEndTag(String qName) {
        if (unclosedStartTag) {
            xml.append("/>");
            unclosedStartTag = false;
        } else {
            xml.append("</").append(qName).append('>');
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) {
        log("characters(%d)", length);
        appendContent(ch, start, length);
    }

    private void appendContent(char[] ch, int start, int length) {
        if (unclosedStartTag) {
            // close start tag, if this is the first content
            xml.append(">");
            unclosedStartTag = false;
        }
        if (withinCData) {
            xml.append(XmlConstants.CDATA_BEGIN).append(ch, start, length).append(XmlConstants.CDATA_END);
        } else {
            appendEscaped(ch, start, length);
        }
    }

    @Override
    public void ignorableWhitespace(char[] ch, int start, int length) {
        log("ignorableWhitespace(%d)", length);
        appendContent(ch, start, length);
    }

    @Override
    public void startPrefixMapping(String prefix, String uri) {
        log("startPrefixMapping('%s', '%s')", prefix, uri);
        globalNamespaces.startPrefixMapping(new NsBinding(prefix, uri));
    }

    @Override
    public void endPrefixMapping(String prefix) {
        log("endPrefixMapping('%s')", prefix);
        globalNamespaces.endPrefixMapping(prefix);
    }

    @Override
    public void setDocumentLocator(Locator locator) {
        log("setDocumentLocator('%s')", locator);

    }

    @Override
    public void skippedEntity(String name) {
        log("skippedEntity('%s')", name);
    }

    @Override
    public void comment(char[] ch, int start, int length) throws SAXException {
        log("comment(%d)", length);
        xml.append(XmlConstants.COMMENT_BEGIN);
        xml.append(ch, start, length);
        xml.append(XmlConstants.COMMENT_END);
    }

    @Override
    public void startCDATA() throws SAXException {
        log("startCDATA()");
        withinCData = true;
    }

    @Override
    public void endCDATA() throws SAXException {
        log("endCDATA()");
        withinCData = false;
    }

    @Override
    public void startDTD(String name, String publicId, String systemId)
            throws SAXException {
        log("startDTD(%s)", name);
    }

    @Override
    public void endDTD() throws SAXException {
        log("endDTD()");
    }

    @Override
    public void startEntity(String name) throws SAXException {
        log("startEntity(%s)", name);
    }

    @Override
    public void endEntity(String name) throws SAXException {
        log("endEntity(%s)", name);
    }

    private void appendNsBindingForQName(String qName) {
        String prefix = extractPrefix(qName);

        if (XmlConstants.XML_NS_PREFIX.equals(prefix) || XmlConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
            // the reserved "xml" and "xmlns" prefixes need not be declared
            return;
        }
        NsBinding globalBinding = globalNamespaces.getNsBinding(prefix);

        if (globalBinding == null) {
            throw new IllegalArgumentException("No namespace binding for QName " + qName);
        }

        NsBinding contextBinding = contextNamespaces.getNsBinding(globalBinding.getPrefix());
        if (contextBinding == null || !contextBinding.getUri().equals(globalBinding.getUri())) {
            // no xmlns declaration exists for the namespace prefix to the namespace. Declare one
            contextNamespaces.startPrefixMapping(globalBinding);
            appendAttribute(globalBinding.getQname(), globalBinding.getUri());
        }
    }

    private void appendAttribute(String qName, String value) {
        xml.append(' ').append(qName).append('=');
        xml.append(XmlEscaper.quotedAttributeText(value));
    }

    private StringBuilder appendEscaped(char[] ch, int start, int length) {
        return appendEscaped(new String(ch, start, length));
    }

    private StringBuilder appendEscaped(String value) {
        xml.append(XmlEscaper.escapeText(value));
        return xml;
    }

    private void log(String format, Object... args) {
        logger.log(format, args);
    }
}
